﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;

namespace Aeon.Emulator.Sound.FM
{
    /// <summary>
    /// Virtual device which emulates OPL3 FM sound.
    /// </summary>
    public sealed class FmSoundCard : IInputPort, IOutputPort
    {
        private const byte Timer1Mask = 0xC0;
        private const byte Timer2Mask = 0xA0;
        private const int DefaultSampleRate = 44100;

        private IntPtr hwnd;
        private DirectSound directSound;
        private DirectSoundBuffer soundBuffer;
        private int currentAddress;
        private readonly FmSynthesizer synth;
        private Thread generateThread;
        private volatile bool endThread;
        private byte timer1Data;
        private byte timer2Data;
        private byte timerControlByte;
        private byte statusByte;
        private bool initialized;
        private bool paused;

        /// <summary>
        /// Initializes a new instance of the FmSoundCard class.
        /// </summary>
        /// <param name="hwnd">Main application window handle.</param>
        public FmSoundCard(IntPtr hwnd)
            : this(DefaultSampleRate, hwnd)
        {
        }
        /// <summary>
        /// Initializes a new instance of the FmSoundCard class.
        /// </summary>
        /// <param name="sampleRate">Sample rate of generated PCM data.</param>
        /// <param name="hwnd">Main application window handle.</param>
        public FmSoundCard(int sampleRate, IntPtr hwnd)
        {
            this.hwnd = hwnd;
            this.synth = new FmSynthesizer(sampleRate);
            this.generateThread = new Thread(this.GenerateWaveforms)
            {
                IsBackground = true,
                Priority = ThreadPriority.AboveNormal
            };
        }

        /// <summary>
        /// Gets the input ports implemented by the device.
        /// </summary>
        IEnumerable<int> IInputPort.InputPorts
        {
            get { return new int[] { 0x388 }; }
        }
        /// <summary>
        /// Gets the output ports implemented by the device.
        /// </summary>
        IEnumerable<int> IOutputPort.OutputPorts
        {
            get { return new int[] { 0x388, 0x389 }; }
        }
        
        /// <summary>
        /// Reads a single byte from one of the device's supported ports.
        /// </summary>
        /// <param name="port">Port from which byte is read.</param>
        /// <returns>Byte read from the specified port.</returns>
        byte IInputPort.ReadByte(int port)
        {
            if((this.timerControlByte & 0x01) != 0x00 && (this.statusByte & Timer1Mask) == 0)
            {
                this.timer1Data++;
                if(this.timer1Data == 0)
                    this.statusByte |= Timer1Mask;
            }
            
            if((this.timerControlByte & 0x02) != 0x00 && (this.statusByte & Timer2Mask) == 0)
            {
                this.timer2Data++;
                if(this.timer2Data == 0)
                    this.statusByte |= Timer2Mask;
            }
            
            return this.statusByte;
        }
        /// <summary>
        /// Reads two bytes from one of the device's supported ports.
        /// </summary>
        /// <param name="port">Port from which bytes are read.</param>
        /// <returns>Bytes read from the specified port.</returns>
        ushort IInputPort.ReadWord(int port)
        {
            return this.statusByte;
        }
        /// <summary>
        /// Writes a single byte to one of the device's supported ports.
        /// </summary>
        /// <param name="port">Port where byte will be written.</param>
        /// <param name="value">Value to write to the port.</param>
        void IOutputPort.WriteByte(int port, byte value)
        {
            if(port == 0x388)
                this.currentAddress = value;
            else if(port == 0x389)
            {
                if(this.currentAddress == 0x02)
                    this.timer1Data = value;
                else if(this.currentAddress == 0x03)
                    this.timer2Data = value;
                else if(this.currentAddress == 0x04)
                {
                    this.timerControlByte = value;
                    if((value & 0x80) == 0x80)
                        this.statusByte = 0;
                }
                else
                {
                    if(!this.initialized)
                        this.Initialize();

                    this.synth.SetRegisterValue(0, currentAddress, value);
                }
            }
        }
        /// <summary>
        /// Writes two bytes to one or two of the device's supported ports.
        /// </summary>
        /// <param name="port">Port where first byte will be written.</param>
        /// <param name="value">Value to write to the ports.</param>
        void IOutputPort.WriteWord(int port, ushort value)
        {
            if(port == 0x388)
            {
                ((IOutputPort)this).WriteByte(0x388, (byte)value);
                ((IOutputPort)this).WriteByte(0x389, (byte)(value >> 8));
            }
        }
        /// <summary>
        /// Invoked when the emulator enters a paused state.
        /// </summary>
        void IVirtualDevice.Pause()
        {
            if(this.initialized && !this.paused)
            {
                this.endThread = true;
                this.generateThread.Join();
                this.paused = true;
            }
        }
        /// <summary>
        /// Invoked when the emulator resumes from a paused state.
        /// </summary>
        void IVirtualDevice.Resume()
        {
            if(this.paused)
            {
                this.endThread = false;
                this.generateThread = new Thread(this.GenerateWaveforms)
                {
                    IsBackground = true,
                    Priority = ThreadPriority.AboveNormal
                };

                this.generateThread.Start();
                this.paused = false;
            }
        }
        /// <summary>
        /// Invoked when the virtual device has been added to a VirtualMachine.
        /// </summary>
        /// <param name="vm">VirtualMachine which owns the device.</param>
        void IVirtualDevice.DeviceRegistered(VirtualMachine vm)
        {
        }
        /// <summary>
        /// Invoked when the device should save its current state to a <see cref="System.IO.Stream"/>.
        /// </summary>
        /// <param name="destination"><see cref="System.IO.Stream"/> to which the state is saved.</param>
        void IVirtualDevice.SaveCurrentState(Stream destination)
        {
        }
        /// <summary>
        /// Invoked when the device should restore its state from a <see cref="System.IO.Stream"/>.
        /// </summary>
        /// <param name="source"><see cref="System.IO.Stream"/> from which the state is restored.</param>
        void IVirtualDevice.RestoreState(Stream source)
        {
        }
        /// <summary>
        /// Releases resources used by the FmSoundCard instance.
        /// </summary>
        public void Dispose()
        {
            if(this.initialized)
            {
                if(!this.paused)
                {
                    this.endThread = true;
                    this.generateThread.Join();
                }
                
                this.soundBuffer.Dispose();
                this.initialized = false;
            }
        }

        /// <summary>
        /// Generates and plays back output waveform data.
        /// </summary>
        private void GenerateWaveforms()
        {
            var buffer = new short[4096];
            this.synth.GetData(buffer, 0, buffer.Length);
            this.soundBuffer.Play(PlaybackMode.LoopContinuously);
            
            while(!this.endThread)
            {
                while(this.soundBuffer.Write(buffer, 0, buffer.Length))
                {
                    this.synth.GetData(buffer, 0, buffer.Length);
                }

                Thread.Sleep(10);
            }
            
            this.soundBuffer.Stop();
        }
        /// <summary>
        /// Performs DirectSound initialization.
        /// </summary>
        private void Initialize()
        {
            this.directSound = DirectSound.GetInstance(hwnd);
            this.soundBuffer = directSound.CreateBuffer(synth.SampleRate, ChannelMode.Monaural, BitsPerSample.Sixteen, TimeSpan.FromSeconds(0.25));
            this.generateThread.Start();
            this.initialized = true;
        }
    }
}
